﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;

using King.Extensions;

namespace King.Reflection {

    public static partial class EnumExtensions {

        //public static string ToCSharpBitMask(this long value) {
        //    return TObject.ToString(CSEnumField.SubstituteBitMask(value));
        //}
        //public static string ToCSharpInitializer(this EnumValue value) {
        //    return TObject.ToString(CSEnumField.SubstituteEnumValue(value));
        //}
        //public static string ToCSharpInitializer(this MaskValue mask) {
        //    return TObject.ToString(CSEnumField.SubstituteEnumMask(mask));
        //}

        public static long ToLong(this Enum e) {
            return EnumInfo.ToLongInternal(e);
        }
        public static T Set<T>(this Enum value, T bits, bool set) {
            if (bits.GetType() != value.GetType())
                throw new ArgumentException();

            var longBits = EnumInfo.ToLongInternal((Enum)(object)bits);
            var longValue = EnumInfo.ToLongInternal(value);
            var longResult = 0L;

            if (set)
                longResult = longValue | longBits;
            else
                longResult = longValue & ~longBits;

            return (T)(object)Enum.ToObject(typeof(T), longResult);
        }
        public static bool IsSet(this Enum value, Enum bits) {
            if (bits.GetType() != value.GetType())
                throw new ArgumentException();

            var longBits = EnumInfo.ToLongInternal(bits);
            var longValue = EnumInfo.ToLongInternal(value);

            if (longBits == 0)
                return longValue == 0;

            return (longValue & longBits) == longBits;
        }
        public static IEnumerable<Enum> GetValues(this Enum @enum) {
            foreach (Enum value in Enum.GetValues(@enum.GetType())) {
                if (@enum.IsSet(value))
                    yield return value;
            }
        }
        public static FieldInfo GetField(this Enum @enum) {
            return @enum.GetType().Get<FieldInfo>(@enum.ToString(), isStatic: true).SingleOrDefault();
        }
        public static IEnumerable<FieldInfo> GetFields(this Enum @enum) {
            foreach (var value in Enum.GetValues(@enum.GetType()).Cast<Enum>()) {
                if (@enum.IsSet(value))
                    yield return GetField(value);
            }
        }
        public static string GetName(this Enum @enum) {
            var field = @enum.GetField();
            return field == null ? null : field.Name;
        }
        public static T GetCustomAttribute<T>(this Enum @enum) {
            return @enum.GetField().GetAttribute<T>();
        }
        public static T ToEnum<T>(this IEnumerable enumerable) {
            return (T)enumerable.ToEnum(typeof(T));
        }
        public static object ToEnum(this IEnumerable enumerable, Type enumType) {
            if (!enumType.IsEnum)
                throw new InvalidOperationException();

            if (enumerable == null)
                throw new ArgumentNullException();

            var objects = enumerable.Cast<object>().ToArray();
            if (objects.Any(o => o == null))
                throw new ArgumentException();

            if (!objects.Any())
                return Enum.ToObject(enumType, 0);

            var types = objects.GroupBy(o => o.GetType()).ToArray();
            if (types.MoreThanOne())
                throw new ArgumentException();

            var type = types.Single().Key;
            if (!type.IsEnum && !type.IsPrimitive)
                throw new ArgumentException();

            var result = Enum.ToObject(enumType, objects.Aggregate(0L, (a, o) => a |= ToLong((Enum)o)));
            return result;
        }
    }

    [AttributeUsage(AttributeTargets.Field)]
    public class MaskAttribute : Attribute {
    }

    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class TransitionAttribute : Attribute {

        public static IEnumerable<EnumTransition> GetTransitions(TransitionAttribute transition) {
            return transition.GetTransitions();
        }

        private IEnumerable<EnumTransition> GetTransitions() {

            // Examples of connonicalization of Transition and Twiddle attributes & invariants
            // Bit Transitions are all To or From *; the bit is either on or off
            // [Twiddle(Bit)] => [Transition From=*, To=Bit] and [Transition From=Bit, To=*]
            // [Transition Between = new[] { X, Y, Z }] => 
            //      [Transition From=X, To=Y], [Transition From=X, To=Z], 
            //      [Transition From=Y, To=X], [Transition From=Y, To=Z],
            //      [Transition From=Z, To=X], [Transition From=Z, To=Y],
            // [Transition To = X] => [Transition From=*, To = X] => [Transition From=Y, To=X], [Transition From=Z, To=X]

            if (Between != null && (From != null || To != null || Twiddle != null))
                throw new ArgumentException(@"Between, To\From, and Tiwddle are exclusive.");
            if ((From != null || To != null) && (Between != null || Twiddle != null))
                throw new ArgumentException(@"Between, To\From, and Tiwddle are exclusive.");
            if (Twiddle != null && (From != null || To != null || Twiddle != null))
                throw new ArgumentException(@"Between, To\From, and Tiwddle are exclusive.");

            var allObjects =
                Between.ToEnumerable().Union(
                From.ToEnumerable()).Union(
                To.ToEnumerable()).Union(
                Twiddle.ToEnumerable()).ToArray();

            if (allObjects.None())
                return Enumerable.Empty<EnumTransition>();

            if (allObjects.GroupBy(o => o.GetType()).MoreThanOne())
                throw new ArgumentException("All elements must be the same type.");
            var type = allObjects.First().GetType();

            var enumInfo = EnumInfo.GetEnumInfo(type);
            if (enumInfo == null)
                throw new ArgumentException("All elements must be enums.");

            var allEnumValues = allObjects.SelectMany(o => enumInfo.Values(o)).ToArray();
            if (allEnumValues.GroupBy(o => o.Mask).MoreThanOne())
                throw new ArgumentException("All enum values must share the same mask.");
            var mask = allEnumValues.First().Mask;

            var isFlags = mask != null ? false : enumInfo.IsFlags;

            var isTransition = Between != null || (From != null && To != null);
            if (isTransition && isFlags)
                throw new ArgumentException("Transitions cannot be between flags. Use Twiddle instead.");

            if (Twiddle != null && !isFlags)
                throw new ArgumentException("Twiddle must contain flags.");

            IEnumerable<EnumTransition> enumTransitions = null;

            if (Twiddle != null)
                enumTransitions =
                    from x in Twiddle.ToEnumerable()
                    from y in Twiddle.ToEnumerable()
                    from t in EnumTransition.GetTransitions(x, y)
                    select t;

            if (Between != null)
                enumTransitions =
                    from x in Between.ToEnumerable()
                    from y in Between.ToEnumerable()
                    from t in EnumTransition.GetTransitions(x, y)
                    select t;

            if (From != null && To != null)
                enumTransitions =
                    from x in From.ToEnumerable()
                    from y in To.ToEnumerable()
                    from t in EnumTransition.GetTransitions(x, y)
                    select t;

            if (From != null)
                enumTransitions =
                    from x in From.ToEnumerable()
                    from e in enumInfo.Values(x)
                    select new EnumTransition(e, null);

            if (To != null)
                enumTransitions =
                    from x in To.ToEnumerable()
                    from e in enumInfo.Values(x)
                    select new EnumTransition(null, e);

            return enumTransitions;
        }

        public object Between { get; set; }
        public object From { get; set; }
        public object To { get; set; }
        public object Twiddle { get; set; }
    }

    public sealed class EnumTransition {

        public static IEnumerable<EnumTransition> GetTransitions(object from, object to) {
            if (from == null || to == null)
                throw new ArgumentException();

            if (from.GetType() != to.GetType())
                throw new ArgumentException();

            var enumInfo = EnumInfo.GetEnumInfo(from.GetType());

            // compute new value set and old value sets
            var oldEnumValues = enumInfo.Values(from).ToArray();
            var newEnumValues = enumInfo.Values(to).ToArray();

            // compute new value set and old value set 
            var diffOldEnumValues = oldEnumValues.Except(newEnumValues).ToArray();
            var diffNewEnumValues = newEnumValues.Except(oldEnumValues).ToArray();

            // check for no differences
            if (!diffNewEnumValues.Any() && !diffOldEnumValues.Any())
                return Enumerable.Empty<EnumTransition>();

            // check for basic enum transition
            if (!enumInfo.IsFlags) {
                var result = new[] {
                    new EnumTransition(
                        diffOldEnumValues.SingleOrDefault(),
                        diffNewEnumValues.SingleOrDefault())
                };

                return result;

            } else {
                var result =
                    // bit set
                   (from o in diffNewEnumValues
                    where o.IsFlags
                    select new EnumTransition(null, o)

                   ).Union(
                    // bit clear
                    from o in diffOldEnumValues
                    where o.IsFlags
                    select new EnumTransition(o, null)

                   ).Union(
                    // enum transition
                    from mask in enumInfo.Masks()
                    let oldEnumValue = diffOldEnumValues.SingleOrDefault(o => o.Mask == mask)
                    let newEnumValue = diffNewEnumValues.SingleOrDefault(o => o.Mask == mask)
                    where oldEnumValue != null || newEnumValue != null
                    select new EnumTransition(oldEnumValue, newEnumValue)

                   ).ToArray();

                return result;
            }
        }
        public static IEnumerable<EnumTransition> GetInvalidTransitions(
            IEnumerable<EnumTransition> validTransitions,
            IEnumerable<EnumTransition> transitions) {

            var goodTransitions =
                from o in transitions
                from x in validTransitions
                where o.From == null || x.From == null || o.From.Contains(x.From)
                where o.To == null || x.To == null || o.To.Contains(x.To)
                select o;
            var badTransitions = transitions.Except(goodTransitions).ToArray();

            return badTransitions;
        }

        private EnumValue m_from;
        private EnumValue m_to;

        internal EnumTransition(EnumValue from, EnumValue to) {
            m_from = from;
            m_to = to;
        }

        public EnumValue From { get { return m_from; } }
        public EnumValue To { get { return m_to; } }
    }

    public sealed class EnumInfo {

        public static string ToString(Enum source) {
            if (source == null || !source.GetType().IsEnum)
                throw new ArgumentException();
            return EnumInfo.GetEnumInfo(source.GetType()).GetValue(source).ToString();
        }
        public static EnumInfo GetEnumInfo(Type type) {
            if (type == null)
                return null;

            EnumInfo enumInfo;
            lock (s_enumInfos)
                s_enumInfos.TryGetValue(type, out enumInfo);

            if (enumInfo == null) {
                var isFlags = type.IsDefined<FlagsAttribute>();
                var newEnumInfo = new EnumInfo(type);
                lock (s_enumInfos) {
                    if (s_enumInfos.TryGetValue(type, out enumInfo))
                        return enumInfo;
                    s_enumInfos[type] = enumInfo = newEnumInfo;
                }
            }

            return enumInfo;
        }

        internal static long ToLongInternal(Enum o) {
            var toLong = GetToLongDelegate(o.GetType());
            return toLong(o);
        }
        internal static Func<object, long> GetToLongDelegate(Type type) {
            var primitiveType = type.IsPrimitive ? type : Enum.GetUnderlyingType(type);

            if (primitiveType == typeof(sbyte)) return o => (long)(sbyte)o;
            if (primitiveType == typeof(short)) return o => (long)(short)o;
            if (primitiveType == typeof(int)) return o => (long)(int)o;
            if (primitiveType == typeof(long)) return o => (long)(long)o;

            if (primitiveType == typeof(byte)) return o => (long)(byte)o;
            if (primitiveType == typeof(ushort)) return o => (long)(ushort)o;
            if (primitiveType == typeof(uint)) return o => (long)(uint)o;
            if (primitiveType == typeof(ulong)) return o => (long)(ulong)o;

            throw new NotSupportedException();
        }
        internal static Func<long, object> GetToEnumDelegate(Type type) {
            var primitiveType = Enum.GetUnderlyingType(type);

            if (primitiveType == typeof(sbyte)) return o => Enum.ToObject(type, (sbyte)o);
            if (primitiveType == typeof(short)) return o => Enum.ToObject(type, (short)o);
            if (primitiveType == typeof(int)) return o => Enum.ToObject(type, (int)o);
            if (primitiveType == typeof(long)) return o => Enum.ToObject(type, (long)o);

            if (primitiveType == typeof(byte)) return o => Enum.ToObject(type, (byte)o);
            if (primitiveType == typeof(ushort)) return o => Enum.ToObject(type, (ushort)o);
            if (primitiveType == typeof(uint)) return o => Enum.ToObject(type, (uint)o);
            if (primitiveType == typeof(ulong)) return o => Enum.ToObject(type, (ulong)o);

            throw new NotSupportedException();
        }

        static EnumInfo() {
            s_enumInfos = new Dictionary<Type, EnumInfo>();
        }

        private static Dictionary<Type, EnumInfo> s_enumInfos;

        private Type m_type;
        private bool m_isFlags;
        private object m_rawDefaultValue;
        private object m_defaultValue;
        private EnumValue m_emptyValue;
        private IEnumerable<EnumValue> m_values;
        private IEnumerable<MaskValue> m_masks;
        private Func<object, long> m_toLongDelegate;
        private Func<long, object> m_toEnumDelegate;

        internal EnumInfo(Type type) {
            if (!type.IsEnum)
                throw new ArgumentException();

            m_type = type;
            m_isFlags = type.IsDefined<FlagsAttribute>();
            m_toLongDelegate = GetToLongDelegate(m_type);
            m_toEnumDelegate = GetToEnumDelegate(m_type);
            m_rawDefaultValue = Enum.GetUnderlyingType(type).GetDefaultValue();
        }

        internal bool Contains(object mask, object value) {
            if (value == null)
                throw new ArgumentNullException();

            if (value.GetType().IsEnum && value.GetType() != Type)
                throw new ArgumentException();

            var longValue = ToLong(value);
            if (longValue.Equals(RawDefaultValue))
                return false;

            var longMask = ToLong(mask);
            return (longMask & longValue) == longValue;
        }

        public Type Type { get { return m_type; } }
        public bool IsFlags { get { return m_isFlags; } }
        public string Name { get { return Type.Name; } }
        public object RawDefaultValue { get { return m_rawDefaultValue; } }
        public object DefaultValue {
            get {
                if (m_defaultValue == null)
                    m_defaultValue = Enum.ToObject(m_type, m_rawDefaultValue);
                return m_defaultValue;
            }
        }
        public EnumValue DefaultEnumValue {
            get {
                if (m_emptyValue == null) {
                    var emptyValue = Values().SingleOrDefault(o => o.IsDefaultValue);
                    if (emptyValue == null)
                        emptyValue = new EnumValue(this, null, null);
                    Interlocked.CompareExchange(ref m_emptyValue, emptyValue, null);
                }
                return m_emptyValue;
            }
        }

        public IEnumerable<EnumValue> Values() {
            if (m_values == null) {
                var maskValues = Masks().SelectMany(o => o.Values());
                var maskFieldInfos = new HashSet<FieldInfo>(maskValues.Select(o => o.FieldInfo));

                var values =
                    from o in m_type.Get<FieldInfo>(isStatic: true)
                    where !o.IsDefined<MaskAttribute>()
                    where !maskFieldInfos.Contains(o)
                    select new EnumValue(this, o, null) into o
                    orderby o.LongValue
                    select o;
                Interlocked.CompareExchange(ref m_values, values.Union(maskValues).ToArray(), null);
            }

            return m_values;
        }
        public EnumValue GetValue(object enumValue) {
            var result = Values(enumValue).GreatestCommonSets((l, r) => l.Contains(r)).ToArray();
            return result.SingleOrDefault();
        }
        public IEnumerable<EnumValue> Values(object enumValue) {
            var result =
                from o in Values().Union(DefaultEnumValue)
                where o.IsSet(enumValue)
                orderby o.LongValue
                select o;

            return result.ToArray();
        }
        public IEnumerable<MaskValue> Masks() {
            if (!IsFlags)
                return Enumerable.Empty<MaskValue>();

            if (m_masks == null) {
                var masks =
                   (from o in Type.Get<FieldInfo>(isStatic: true)
                    where o.IsDefined<MaskAttribute>()
                    select new MaskValue(this, o))
                    .ToArray();

                Interlocked.CompareExchange(ref m_masks, masks, null);
            }
            return m_masks;
        }
        public IEnumerable<EnumMemberInfo> Members() {
            return Values().Cast<EnumMemberInfo>().Union(Masks().Cast<EnumMemberInfo>()).ToArray();
        }

        public long ToLong(object value) { return m_toLongDelegate(value); }
        public object ToObject(long value) { return m_toEnumDelegate(value); }
        public object ToObject(IEnumerable<EnumValue> flags) {
            if (flags == null)
                throw new ArgumentNullException();

            return flags.Select(o => o.Value).ToEnum(Type);
        }

        public sealed override string ToString() {
            return Name;
        }
    }

    public abstract class EnumMemberInfo : ICustomAttributeProvider {

        private EnumInfo m_enumInfo;
        private FieldInfo m_fieldInfo;
        private string m_name;
        private object m_rawValue;
        private object m_value;

        internal EnumMemberInfo(EnumInfo enumInfo, FieldInfo fieldInfo) {
            m_enumInfo = enumInfo;

            if (fieldInfo == null) {
                m_rawValue = EnumInfo.RawDefaultValue;

            } else {
                m_fieldInfo = fieldInfo;
                m_rawValue = m_fieldInfo.GetRawConstantValue();
                m_name = fieldInfo.Name;
            }
        }

        public string Name { get { return m_name; } }
        public object Value {
            get {
                if (m_value == null)
                    m_value = Enum.ToObject(m_enumInfo.Type, m_rawValue);
                return m_value;
            }
        }
        public object RawValue { get { return m_rawValue; } }
        public long LongValue { get { return EnumInfo.ToLong(m_rawValue); } }
        public EnumInfo EnumInfo { get { return m_enumInfo; } }
        public FieldInfo FieldInfo { get { return m_fieldInfo; } }

        public object[] GetCustomAttributes(bool inherit) {
            return m_fieldInfo.GetCustomAttributes(inherit);
        }
        public object[] GetCustomAttributes(Type attributeType, bool inherit) {
            return m_fieldInfo.GetCustomAttributes(attributeType, inherit);
        }
        public bool IsDefined(Type attributeType, bool inherit) {
            return m_fieldInfo.IsDefined(attributeType, inherit);
        }
    }
    public sealed class MaskValue : EnumMemberInfo {

        private IEnumerable<EnumValue> m_values;

        internal MaskValue(EnumInfo enumInfo, FieldInfo fieldInfo)
            : base(enumInfo, fieldInfo) {
        }

        private bool Contains(object enumValue) {
            return EnumInfo.Contains(RawValue, enumValue);
        }

        public bool Contains(EnumValue enumMemberInfo) {
            return Contains(enumMemberInfo.Value);
        }
        public IEnumerable<EnumValue> Values() {
            if (m_values == null) {
                var values =
                    from o in EnumInfo.Type.Get<FieldInfo>(isStatic: true)
                    where !o.IsDefined<MaskAttribute>()
                    where Contains(o.GetRawConstantValue())
                    select new EnumValue(EnumInfo, o, this)
                        into o
                        orderby o.LongValue select o;
                Interlocked.CompareExchange(ref m_values, values.ToArray(), null);
            }

            return m_values;
        }

        //public sealed override string ToString() {
        //    return EnumInfo.Name + "." + Name + " = " + this.ToCSharpInitializer();
        //}
    }
    public sealed class EnumValue : EnumMemberInfo {

        public static explicit operator long(EnumValue source) {
            return source.LongValue;
        }

        public static IEnumerable<EnumValue> GetEnumValue(object value) {
            if (value == null)
                return null;

            var enumInfo = EnumInfo.GetEnumInfo(value.GetType());
            if (enumInfo == null)
                return null;

            return enumInfo.Values(value);
        }

        private MaskValue m_mask;
        private bool m_isFlags;
        private IEnumerable<EnumValue> m_values;

        internal EnumValue(EnumInfo enumInfo, FieldInfo fieldInfo, MaskValue mask)
            : base(enumInfo, fieldInfo) {

            m_mask = mask;

            m_isFlags = enumInfo.IsFlags;
            if (mask != null)
                m_isFlags = false;
        }

        public MaskValue Mask { get { return m_mask; } }
        public bool IsUberValue { get { return LongValue == -1; } }
        public bool IsDefaultValue { get { return RawValue.Equals(EnumInfo.RawDefaultValue); } }
        public IEnumerable<EnumValue> ContainingValues() {
            var result =
                from o in EnumInfo.Values()
                where o.Contains(this)
                orderby o.LongValue
                select o;

            return result.ToArray();
        }

        // interesting if IsFlags
        public bool IsFlags {
            get { return m_isFlags; }
        }
        public bool Contains(object enumValue) {
            if (!IsFlags)
                return EnumInfo.Contains(0, enumValue);
            return EnumInfo.Contains(RawValue, enumValue);
        }
        public bool Contains(EnumValue member) {
            if (!IsFlags)
                return false;
            return Values().Contains(member);
        }
        public IEnumerable<EnumValue> Values() {
            if (!IsFlags)
                return Enumerable.Empty<EnumValue>();

            if (m_values == null) {
                var values = EnumInfo.Values().Where(o => o != this && Contains(o.RawValue)).ToArray();
                Interlocked.CompareExchange(ref m_values, values, null);
            }
            return m_values;
        }
        public IEnumerable<EnumValue> Values(object enumValue) {
            if (!IsFlags)
                return Enumerable.Empty<EnumValue>();

            var result =
                from o in Values()
                where o.IsSet(enumValue)
                select o;

            return result.ToArray();
        }
        public bool IsSet(object enumValue) {
            if (enumValue == null)
                throw new ArgumentException();

            var enumType = enumValue.GetType();
            if (enumType != EnumInfo.Type && !enumType.IsPrimitive)
                throw new ArgumentException();

            var value = (long)this;
            var target = EnumInfo.ToLong(enumValue);

            if (target == 0)
                return IsDefaultValue;

            if (IsDefaultValue)
                return false;

            if (Mask != null)
                target &= Mask.LongValue;

            if (target == 0)
                return false;

            var result = target == value;
            if (IsFlags)
                result = (target & value) == value;

            return result;
        }

        //public sealed override string ToString() {
        //    if (FieldInfo == null)
        //        return "default(" + EnumInfo.Name + ")";
        //    return EnumInfo.Name + "." + Name + " = " + this.ToCSharpInitializer();
        //}
    }
}
